SQLServerCentral Article

Why Clone Fails: Discovering the Hardware Beneath Azure SQL MI

,

Microsoft describes Azure SQL Managed Instance (MI) as offering near-full SQL Server compatibility with more flexibility than typical PaaS. So naturally, I wondered: how much control does a developer really have?

I set out to find out, not by reading the documentation, but by poking around under the hood. The mission? To see whether SQL MI could support fast, lightweight cloning using the same tricks I'd used before: spinning up parent-child VHDs, mounting them, and leveraging .mdf files, but now from within the SQL MI instance. If SQL MI was just "SQL Server in a VM," it might work. If it was a fully locked-down black box, it definitely wouldn't.

The answer turned out to be neither and more interesting than expected.

My first trick in the bag: xp_cmdshell

We've all leaned on xp_cmdshell at some point, whether it was a quick and dirty backup move, firing off an OS-level check, or just a way to "escape the box." But what happens when that escape hatch is welded shut?

Welcome to SQL Managed Instance (MI), a powerful cloud offering that brings the best of SQL Server to the platform-as-a-service world, but with some surface-level OS access removed. And yes, xp_cmdshell is gone.

But that didn't stop me...

Peeking Behind the PaaS Curtain

So I turned to the only tool still in reach: SQL Agent. With no xp_cmdshell and no RDP, I started inspecting the environment from the inside — one job step at a time. Without xp_cmdshell, you might think your options are limited, but the SQL Agent is surprisingly capable. It can execute both CMD and PowerShell "job step types" and return output right into the SQL Agent job history logs or results table.

A screenshot of a computer AI-generated content may be incorrect.

I started with some basic commands in my job steps:

Drive Listings

wmic logicaldisk get name
Name
----
C:
D:

Directory Scans

Get-ChildItem -Path C:\, D:\ | Select-Object FullName
FullName
--------
C:\03CC4CC8-D99F-4CC0-8946-ED06CD486B84
C:\Dumps
C:\Installer
C:\Logs
C:\Program Files
C:\Program Files (x86)
C:\ScriptsForContainer
C:\SFApplications
C:\SFFabricBin
C:\SFFabricLog
C:\SFPackageRoot
C:\Users
C:\VCRedist
C:\Windows
C:\License.txt
D:\CollectGuestLogsTemp
D:\HDD
D:\MonitoringDataDirectory
D:\SFVmExtnData
D:\WER
D:\WFRoot
D:\DATALOSS_WARNING_README.txt
D:\GcsConfCommt.txt

Local Users

Get-LocalUser
Name Enabled Description
---- ------- -----------
Administrator True Built-in account for administering the computer/domain
DefaultAccount False A user account managed by the system.
Guest False Built-in account for guest access to the computer/domain
replAgentUser True replication
sqlAgentUser True replication
WDAGUtilityAccount False A user account managed and used by the system for Windows Defender Application Guard scena...

Services

Get-Service | Where Status -eq Running
Status Name DisplayName
------ ---- -----------
Running cexecsvc Container Execution Agent
Running CoreMessagingRe... CoreMessaging
Running CryptSvc Cryptographic Services
Running DcomLaunch DCOM Server Process Launcher
Running Dhcp DHCP Client
Running DiagTrack Connected User Experiences and Tele...
Running Dnscache DNS Client
Running EventLog Windows Event Log
Running EventSystem COM+ Event System
Running GenevaContainer... Geneva Container Shim
Running gpsvc Group Policy Client
Running iphlpsvc IP Helper
Running KeyIso CNG Key Isolation
Running LanmanWorkstation Workstation
Running LSM Local Session Manager
Running nsi Network Store Interface Service
Running ProfSvc User Profile Service
Running RpcEptMapper RPC Endpoint Mapper
Running RpcSs Remote Procedure Call (RPC)
Running SamSs Security Accounts Manager
Running Schedule Task Scheduler
Running SENS System Event Notification Service
Running StateRepository State Repository Service
Running SysMain SysMain
Running SystemEventsBroker System Events Broker
Running TimeBrokerSvc Time Broker
Running UserManager User Manager
Running UsoSvc Update Orchestrator Service
Running WinHttpAutoProx... WinHTTP Web Proxy Auto-Discovery Se...
Running Winmgmt Windows Management Instrumentation
Running WinRM Windows Remote Management (WS-Manag...

At this point you've might already ask yourself, where are the SQL* Services…but I continued my investigation.

Environment Variables

Get-ChildItem Env:
Name Value
---- -----
ALLUSERSPROFILE C:\ProgramData
APPDATA C:\Users\ContainerAdministrator\AppData\Roaming
CheckConnectivityOnContaine... 1
CommonProgramFiles C:\Program Files\Common Files
CommonProgramFiles(x86) C:\Program Files (x86)\Common Files
CommonProgramW6432 C:\Program Files\Common Files
COMPLUS_MDA InvalidVariant;RaceOnRCWCleanup;InvalidFunctionPointerInDelegate;InvalidMemberDeclarat...
COMPUTERNAME CHNKHERPO1VUMRQ
ComSpec C:\Windows\system32\cmd.exe
DriverData C:\Windows\System32\Drivers\DriverData
EnableContainerShim 1
EnableSeLockMemoryPrivilege 1
…
USERDOMAIN User Manager
USERNAME ContainerAdministrator
USERPROFILE C:\Users\ContainerAdministrator
windir C:\Windows

As you can see, the username (ContainerAdministrator) and values like EnableContainerShim and CheckConnectivityOnContainerHealth made it clear: this was a containerized environment, not a traditional host.

Full System Info

So, what did the system info tell us?

systeminfo
Host Name: CHNKHERPO1VUMRQ
OS Name: Microsoft Windows Server 2022 Datacenter
OS Version: 10.0.20348 N/A Build 20348
OS Manufacturer: Microsoft Corporation
OS Configuration: Standalone Server
OS Build Type: Multiprocessor Free
Registered Owner: N/A
Registered Organization: N/A
Product ID: 00454-60000-00001-AA216
Original Install Date: 8/3/2023, 5:02:23 PM
System Boot Time: 6/13/2025, 4:02:08 PM
System Manufacturer: Microsoft Corporation
System Model: Virtual Machine
System Type: x64-based PC
Processor(s): 1 Processor(s) Installed.
[01]: Intel64 Family 6 Model 85 Stepping 7 GenuineIntel ~2594 Mhz
BIOS Version: Microsoft Corporation Hyper-V UEFI Release v4.1, 5/13/2024
Windows Directory: C:\Windows
System Directory: C:\Windows\system32
Boot Device: \Device\HarddiskVolume1
System Locale: en-us;English (United States)
Input Locale: en-us;English (United States)
Time Zone: (UTC) Coordinated Universal Time
Total Physical Memory: 22,477 MB
Available Physical Memory: 11,063 MB
Virtual Memory: Max Size: 27,597 MB
Virtual Memory: Available: 14,691 MB
Virtual Memory: In Use: 12,906 MB
Page File Location(s): D:\pagefile.sys
Domain: WORKGROUP
Logon Server: N/A
Hotfix(s): 3 Hotfix(s) Installed.
[01]: KB5028956
[02]: KB5029250
[03]: KB5029395
Network Card(s): N/A
Hyper-V Requirements: A hypervisor has been detected. Features required for Hyper-V will not be displayed.

Each job gave me a little more insight. This wasn't a stripped-down SQL container on Linux. It was clearly Windows Server 2022 Datacenter, or so I thought.

Installed programs

Then I listed the installed programs.

wmic product get name,version

That's when things got weird.

Name Version
---------------------------------------------- -------------------
Microsoft OLE DB Driver for SQL Server 18.7.4.0
Microsoft OLE DB Driver 19 for SQL Server 19.3.0.0
Microsoft Visual C++ 2022 X64 Minimum Runtime 14.42.34433
Microsoft Visual C++ 2022 X86 Minimum Runtime 14.42.34433
Microsoft Visual C++ 2022 X64 Additional Runtime 14.42.34433
Microsoft Visual C++ 2022 X86 Additional Runtime 14.42.34433

That's it. No full SQL Server installation in sight. And yet, SQL Server was running.

It quickly became clear this wasn't a normal VM (or IaaS in disguise), this was a highly curated container environment, orchestrated by Azure, and built for isolation. Environment variables like Fabric_* hinted at Azure Service Fabric under the hood or was it a preparation for the Fabric?

At this point, my original goal — using VHD-based clone techniques — was starting to feel naïve.

But I had to try anyway.

Trying the Clone Trick Anyway: VHD Mounting Attempts

In a traditional setup, I would create a dynamically expanding VHD, format it, mount it, and use it as a database mount point.

So I tried exactly that, using PowerShell in a SQL Agent job:

New-VHD -Path C:\vhd\parent.vhdx -Dynamic -SizeBytes 2048GB |
Mount-VHD -Passthru |
Initialize-Disk -Passthru |
New-Partition -UseMaximumSize |
Format-Volume -FileSystem NTFS -Confirm:$false -Force

Result: ? The Hyper-V Management Tools could not access an expected WMI class.

I had to fell back to diskpart:

diskpart

Result: ? Virtual Disk Service error: The service failed to initialize.

Turns out diskpart depends on the Virtual Disk Service, which simply isn't available in the OS driving SQL MI.

You also can't install it, Install-WindowsFeature, doesn't work either here.

So: no Hyper-V, no VDS, no disk tools. My original plan was dead in the water.

But What Can You Do?

Just as I was ready to give up, I suddenly realized...

My simple command jobs ran successfully and gave me full output. So it turns out: even in a restricted MI environment, SQL Server Agent has enough rights to run native OS commands, both CMD and PowerShell.

That opened the door to a whole new line of questions. For example: Could I list the active processes running inside the container? Could I finally find the sqlservr process itself, the very engine hosting the database? And what would happen if I tried to kill it?

I ran a quick PowerShell job:

Get-Process | Where-Object { $_.ProcessName -like "sqlserv*" } 
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
1542 987 1991728 856820 41.23 19832 3 sqlservr

And there it was. Finally, sqlservr was visible for me, alive and well!

Then I took the next (arguably reckless) step:

Stop-Process -Name sqlservr -Force

The job succeeded, my query window died instantly on me…

A screenshot of a computer AI-generated content may be incorrect.

…and SSMS froze:

A screenshot of a computer AI-generated content may be incorrect.

The process clearly vanished. But within minutes, SQL MI was automatically respawned by (I assume) the container's orchestration layer, and the sqlservr process was back *:

Get-Process | Where-Object { $_.ProcessName -like "sqlserv*" }
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
1602 970 1753472 851444 16.72 19244 3 sqlservr

*) To be transparent, restarting the agent took a little more than a few minutes. But eventually I came past this message:

A screenshot of a computer AI-generated content may be incorrect.

The good old sqlservr was back but with a new PID! Well actually, if I looked closer every process seem to have a new PID! Could this be a shiny new container instance?

A screen shot of a computer AI-generated content may be incorrect.

The implications were clear: MI was more resilient and more containerized than I had imagined. Even killing the SQL Server process didn't break anything (permanently). It simply came back, silently reborn, as if nothing had happened.

Where Does That Leave Us?

This exploration taught me what SQL MI is not:

  • Not a general-purpose Windows VM
  • Not a Hyper-V host
  • Not a clone-friendly IaaS environment

But it also showed what it is:

  • A containerized SQL runtime with just enough exposed shell to poke at
  • An environment where you can run OS-level queries via Agent jobs
  • A great place to experiment, but messing to hard will get you a fresh container ??

What's Next?

In Part 2, I'll try something riskier.

If I can run PowerShell… can I introduce external code?

  • No shell exploits.
  • No SQLCLR.
  • Just documented features and strategic file placement.

I'll explore whether you can upload a binary into a user database, sync it into MI, and convince the container OS to run it, from the inside.

We're not done. We're just getting started!

Bonus: A Handy Tool for PowerShell Execution

As a side project, and to save myself a ton of frustration, I built a stored procedure that lets you run PowerShell via one-time SQL Agent Job and retrieve the output in near real time, even when xp_cmdshell is disabled.

Why? Because experimenting through manually scheduled Agent jobs and digging through their logs is... painful. It's slow, error-prone, and just not fun when you're iterating on commands.

So I wrote dbo.RunPowerShellViaAgent.

CREATE PROCEDURE dbo.RunPowerShellViaAgent @PowerShellCommand NVARCHAR(MAX)
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @JobName SYSNAME = N'ps_exec_' + CONVERT(VARCHAR(36), NEWID());
    DECLARE @StepId INT = 1;
    DECLARE @JobId UNIQUEIDENTIFIER;
    DECLARE @output NVARCHAR(MAX) = N'';
    DECLARE @finalOutput TABLE
    (
        TerminalLine NVARCHAR(MAX)
    )
    BEGIN TRY
        -- Create the SQL Agent Job
        EXEC msdb.dbo.sp_add_job @job_name = @JobName,
                                 @enabled = 1,
                                 @start_step_id = @StepId,
                                 @notify_level_eventlog = 0,
                                 @job_id = @JobId OUTPUT;
        -- Add a PowerShell Step
        EXEC msdb.dbo.sp_add_jobstep @job_id = @JobId,
                                     @step_id = 1,
                                     @step_name = N'Run PowerShell',
                                     @subsystem = N'PowerShell',
                                     @command = @PowerShellCommand,
                                     @on_success_action = 1,
                                     @on_fail_action = 2,
                                     @flags = 8;

        -- Add the job to the server
        EXEC msdb.dbo.sp_add_jobserver @job_id = @JobId,
                                       @server_name = N'(LOCAL)';
        -- Start the job synchronously
        EXEC msdb.dbo.sp_start_job @job_id = @JobId;
        -- Wait for the job to finish
        WHILE EXISTS
              (
                  SELECT 1
                  FROM msdb.dbo.sysjobactivity a
                      JOIN msdb.dbo.sysjobs j
                          ON a.job_id = j.job_id
                  WHERE j.name = @JobName
                        AND a.stop_execution_date IS NULL
              )
        BEGIN
            WAITFOR DELAY '00:00:01';
        END
        -- Get step output
        SELECT @output = l.log
        FROM msdb.dbo.sysjobstepslogs l
            JOIN msdb.dbo.sysjobsteps s
                ON l.step_uid = s.step_uid
            JOIN msdb.dbo.sysjobs j
                ON j.job_id = s.job_id
        WHERE j.name = @JobName;
        -- Include original command as first line
        INSERT INTO @finalOutput
        (
            TerminalLine
        )
        VALUES
        (@PowerShellCommand);
        -- Use STRING_SPLIT to avoid XML issues
        IF @output IS NOT NULL
        BEGIN
          INSERT INTO @finalOutput (TerminalLine)
          SELECT value
            FROM STRING_SPLIT(
                     REPLACE(cast(@output as nvarchar(max)),
                             CAST( CHAR(13) + CHAR(10) as nvarchar(max)),
                             CAST(CHAR(10) as nvarchar(max))), CHAR(13), CHAR(10)), CHAR(13), CHAR(10)), CHAR(10));
        END
    END TRY
    BEGIN CATCH
        -- Capture error details if needed (optional)
        DECLARE @Err NVARCHAR(MAX) = ERROR_MESSAGE();
        INSERT INTO @finalOutput
        (
            TerminalLine
        )
        VALUES
        ('ERROR: ' + @Err);
    END CATCH
    -- Ensure the job is cleaned up
    BEGIN TRY
        EXEC msdb.dbo.sp_delete_job @job_name = @JobName,
                                    @delete_unused_schedule = 1;
    END TRY
    BEGIN CATCH
    -- Ignore errors during cleanup
    END CATCH
    -- Output result
    SELECT TerminalLine
    FROM @finalOutput;
END;


With this, you can do things like:

EXEC dbo.RunPowerShellViaAgent N'Get-ChildItem C:\ | Out-String'

And get formatted, readable terminal output, right inside SSMS.

It's fast, flexible, and perfect for digging into MI's container OS or really, any SQL Server instance where the Agent has enough (or maybe a bit too much) privilege.

A screenshot of a computer AI-generated content may be incorrect.

 

Rate

5 (2)

You rated this post out of 5. Change rating

Share

Share

Rate

5 (2)

You rated this post out of 5. Change rating